// Client network command handlers for HaxServ
//
// Written by: Test_User <hax@andrewyu.org>
//
// This is free and unencumbered software released into the public
// domain.
//
// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.
//
// In jurisdictions that recognize copyright laws, the author or authors
// of this software dedicate any and all copyright interest in the
// software to the public domain. We make this dedication for the benefit
// of the public at large and to the detriment of our heirs and
// successors. We intend this dedication to be an overt act of
// relinquishment in perpetuity of all present and future rights to this
// software under copyright law.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.

#include <gnutls/gnutls.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#include "commands.h"
#include "network.h"
#include "config.h"
#include "types.h"
#include "table.h"
#include "tls.h"

struct table client_network_commands = {0};
struct string client_nick = {0};
uint8_t client_connected;

int add_local_client(struct string uid, struct string nick_arg, struct string vhost_arg, struct string ident_arg, struct string realname_arg, time_t timestamp, char fake_cert) {
	if (has_table_index(user_list, uid))
		return 1;

	struct user_info *user = malloc(sizeof(*user));
	if (!user)
		goto add_local_client_fail_user;

	struct string server = {
		.data = malloc(3),
		.len = 3,
	};
	if (!server.data)
		goto add_local_client_fail_server;

	memcpy(server.data, "1HC", 3);

	struct string hostname = {
		.data = malloc(vhost_arg.len),
		.len = vhost_arg.len,
	};
	if (!hostname.data)
		goto add_local_client_fail_hostname;

	memcpy(hostname.data, vhost_arg.data, hostname.len);

	struct string vhost = {
		.data = malloc(vhost_arg.len),
		.len = vhost_arg.len,
	};
	if (!vhost.data)
		goto add_local_client_fail_vhost;

	memcpy(vhost.data, vhost_arg.data, vhost.len);

	struct string ident = {
		.data = malloc(ident_arg.len),
		.len = ident_arg.len,
	};
	if (!ident.data)
		goto add_local_client_fail_ident;

	memcpy(ident.data, ident_arg.data, ident.len);

	struct string ip = {
		.data = malloc(9),
		.len = 9,
	};
	if (!ip.data)
		goto add_local_client_fail_ip;

	memcpy(ip.data, "/dev/null", 9);

	struct string realname = {
		.data = malloc(realname_arg.len),
		.len = realname_arg.len,
	};
	if (!realname.data)
		goto add_local_client_fail_realname;

	memcpy(realname.data, realname_arg.data, realname.len);

	struct string nick = {
		.data = malloc(nick_arg.len),
		.len = nick_arg.len,
	};
	if (!nick.data)
		goto add_local_client_fail_nick;

	memcpy(nick.data, nick_arg.data, nick.len);

	*user = (struct user_info){
		.nick_ts = (uint64_t)timestamp,
		.user_ts = (uint64_t)timestamp,
		.server = server,
		.nick = nick,
		.hostname = hostname,
		.vhost = vhost,
		.ident = ident,
		.ip = ip,
		.realname = realname,
		.opertype = {.data = malloc(0), .len = 0},
		.metadata = {.array = malloc(0), .len = 0},
	};

	set_table_index(&user_list, uid, user);

	SEND(STRING("GLOADMODULE m_servprotect\n")); // required for the +k we're about to use

	char string_time[21];
	snprintf(string_time, 21, "%ld", timestamp);
	SEND(STRING("UID "));
	SEND(uid);
	SEND(STRING(" "));
	SEND(NULSTR(string_time));
	SEND(STRING(" "));
	SEND(nick);
	SEND(STRING(" "));
	SEND(vhost);
	SEND(STRING(" "));
	SEND(vhost);
	SEND(STRING(" "));
	SEND(ident);
	SEND(STRING(" "));
	SEND(ip);
	SEND(STRING(" "));
	SEND(NULSTR(string_time));
	SEND(STRING(" +k :"));
	SEND(realname);
	SEND(STRING("\n"));
	if (fake_cert) {
		SEND(STRING(":1HC METADATA "));
		SEND(uid);
		SEND(STRING(" ssl_cert :vTrse "));
		SEND(client_cert);
		SEND(STRING("\n"));
	}
	if (!STRING_EQ(uid, STRING("1HC000000"))) { // Don't oper haxserv, because echo is unprivileged
		SEND(STRING(":"));
		SEND(uid);
		SEND(STRING(" OPERTYPE "));
		SEND(opertype);
		SEND(STRING("\n"));
	}

	return 0;

	add_local_client_fail_nick:
	free(realname.data);
	add_local_client_fail_realname:
	free(ip.data);
	add_local_client_fail_ip:
	free(ident.data);
	add_local_client_fail_ident:
	free(vhost.data);
	add_local_client_fail_vhost:
	free(hostname.data);
	add_local_client_fail_hostname:
	free(server.data);
	add_local_client_fail_server:
	free(user);
	add_local_client_fail_user:

	WRITES(2, STRING("OOM! (add_local_client)\n"));

	return 1;
}

int client_nick_handler(uint64_t argc, struct string *argv) {
	if (argc < 1)
		return 1;

	void *tmp = malloc(argv[0].len);
	if (!tmp)
		return 1;

	void *name_for_tables;
	if (client_connected) {
		name_for_tables = malloc(argv[0].len);
		if (!name_for_tables) {
			free(tmp);
			return 1;
		}

		memcpy(name_for_tables, argv[0].data, argv[0].len);
	}

	memcpy(tmp, argv[0].data, argv[0].len);

	if (client_connected) {
		SENDCLIENT(STRING(":"));
		SENDCLIENT(client_nick);
		SENDCLIENT(STRING("!e@e NICK :"));
		SENDCLIENT(argv[0]);
		SENDCLIENT(STRING("\r\n"));
	}

	free(client_nick.data);

	client_nick.data = tmp;
	client_nick.len = argv[0].len;

	if (client_connected) {
		struct user_info *client = get_table_index(user_list, STRING("1HC000001"));
		if (client) {
			free(client->nick.data);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
			client->nick.data = name_for_tables;	// Will not be used uninitialized, ignore the compiler's complaint here
#pragma GCC diagnostic pop					// Compiler sees global variable and assumes it may be changed in one of the functions called between checks
			client->nick.len = argv[0].len;
		} else {
			free(name_for_tables);
			WRITES(2, STRING("Client connected but client data missing!\n"));
			return 1;
		}

		SEND(STRING(":1HC000001 NICK "));
		SEND(client_nick);
		SEND(STRING(" "));
		char current_time[22];
		snprintf(current_time, 22, "%ld", time(NULL));
		SEND(NULSTR(current_time));
		SEND(STRING("\n"));
	}

	return 0;
}

int client_user_handler(uint64_t argc, struct string *argv) {
	if (argc < 4)
		return 1;

	if (client_nick.len == 0)
		return 1;

	if (add_local_client(STRING("1HC000001"), client_nick, client_hostmask, argv[0], argv[3], time(NULL), 1) != 0)
		return 1;

	SENDCLIENT(STRING(":"));
	SENDCLIENT(server_name);
	SENDCLIENT(STRING(" 001 "));
	SENDCLIENT(client_nick);
	SENDCLIENT(STRING(" :Welcome to the Rexnet IRC Network\r\n:"));
	SENDCLIENT(server_name);
	SENDCLIENT(STRING(" 002 "));
	SENDCLIENT(client_nick);
	SENDCLIENT(STRING(" :Your host is "));
	SENDCLIENT(server_name);
	SENDCLIENT(STRING(", running a totally not sus IRCd\r\n:"));
	SENDCLIENT(server_name);
	SENDCLIENT(STRING(" 003 "));
	SENDCLIENT(client_nick);
	SENDCLIENT(STRING(" :This server was created 02:51:36 Apr 03 2023\r\n:"));
	SENDCLIENT(server_name);
	SENDCLIENT(STRING(" 004 "));
	SENDCLIENT(client_nick);
	SENDCLIENT(STRING(" "));
	SENDCLIENT(server_name);
	SENDCLIENT(STRING(" InspIRCd-3 BDGHILNORSTWcdghikorswxz ABCDEFGHIJKLMNOPQRSTXYZabcdefghijklmnopqrstuvwz :BEFHIJLXYZabdefghjkloqvw\r\n:"));
	SENDCLIENT(server_name);
	SENDCLIENT(STRING(" 005 "));
	SENDCLIENT(client_nick);
	SENDCLIENT(STRING(" ACCEPT=100 AWAYLEN=200 BOT=B CALLERID=g CASEMAPPING=ascii CHANLIMIT=#:20 CHANMODES=IXZbegw,k,BEFHJLdfjl,ACDGKMNOPQRSTcimnprstuz CHANNELLEN=60 CHANTYPES=# ELIST=CMNTU ESILENCE=CcdiNnPpTtx EXCEPTS=e :are supported by this server\r\n:"));
	SENDCLIENT(server_name);
	SENDCLIENT(STRING(" 005 "));
	SENDCLIENT(client_nick);
	SENDCLIENT(STRING(" EXTBAN=,ACNOQRSTUacjmnprswz HOSTLEN=64 INVEX=I KEYLEN=32 KICKLEN=300 LINELEN=512 MAXLIST=I:1000,X:1000,b:1000,e:1000,g:1000,w:1000 MAXTARGETS=20 MODES=20 MONITOR=30 NAMELEN=130 NAMESX NETWORK=LibreIRC :are supported by this server\r\n:"));
	SENDCLIENT(server_name);
	SENDCLIENT(STRING(" 005 "));
	SENDCLIENT(client_nick);
	SENDCLIENT(STRING(" NICKLEN=30 OVERRIDE=O PREFIX=(Yqaohv)!~&@%+ REMOVE SAFELIST SECURELIST=60 SILENCE=100 STATUSMSG=!~&@%+ TOPICLEN=330 UHNAMES USERIP USERLEN=10 USERMODES=,,s,BDGHILNORSTWcdghikorwxz :are supported by this server\r\n:"));
	SENDCLIENT(server_name);
	SENDCLIENT(STRING(" 005 "));
	SENDCLIENT(client_nick);
	SENDCLIENT(STRING(" WATCH=32 WHOX :are supported by this server\r\n"));

	client_connected = 1;

	return 0;
}

int client_join_handler(uint64_t argc, struct string *argv) {
	if (argc < 1)
		return 1;

	time_t ctime = time(NULL);

	struct string channels = argv[0];
	while (1) {
		char current_time_nulstr[22];
		uint64_t current_time;
		if (ctime < 0) {
			WRITES(2, STRING("Please check your clock.\n"));
			return 1;
		}
		current_time = (uint64_t)ctime;

		uint64_t offset = 0;

		while (offset < channels.len && channels.data[offset] != ',')
			offset++;

		uint64_t oldlen = channels.len;
		channels.len = offset;

		struct channel_info *channel_info = get_table_index(channel_list, channels);
		if (!channel_info) {
			channel_info = malloc(sizeof(*channel_info));
			if (!channel_info) {
				WRITES(2, STRING("OOM! (client_join)\n"));
				return 1;
			}
			*channel_info = (struct channel_info){
				.ts = current_time,
				.topic = {.data = malloc(0), .len = 0},
				.topic_ts = 0,
				.modes = {.array = malloc(0), .len = 0},
				.user_list = {.array = malloc(0), .len = 0},
				.metadata = {.array = malloc(0), .len = 0},
			};

			set_table_index(&channel_list, channels, channel_info);
		}
		if (channel_info->ts < current_time)
			current_time = channel_info->ts;

		snprintf(current_time_nulstr, 22, "%lu", current_time);

		SENDCLIENT(STRING(":"));
		SENDCLIENT(client_nick);
		SENDCLIENT(STRING("!e@e JOIN :"));
		SENDCLIENT(channels);
		for (uint64_t i = 0; i < channel_info->user_list.len; i++) {
			struct string user;
			struct user_info *info = channel_info->user_list.array[i].ptr;
			if (info)
				user = info->nick;
			else
				user = user_list.array[i].name;

			if (i%5 != 0) {
				SENDCLIENT(STRING(" "));
				SENDCLIENT(user);
			} else {
				SENDCLIENT(STRING("\r\n:"));
				SENDCLIENT(server_name);
				SENDCLIENT(STRING(" 353 "));
				SENDCLIENT(client_nick);
				SENDCLIENT(STRING(" = "));
				SENDCLIENT(channels);
				SENDCLIENT(STRING(" :"));
				SENDCLIENT(user);
			}
		}
		SENDCLIENT(STRING("\r\n:"));
		SENDCLIENT(server_name);
		SENDCLIENT(STRING(" 366 "));
		SENDCLIENT(client_nick);
		SENDCLIENT(STRING(" "));
		SENDCLIENT(channels);
		SENDCLIENT(STRING(" :End of /NAMES list.\r\n"));

		SEND(STRING(":1HC FJOIN "));
		SEND(channels);
		SEND(STRING(" "));
		SEND(NULSTR(current_time_nulstr));
		SEND(STRING(" + :,1HC000001\n"));

		set_table_index(&(channel_info->user_list), STRING("1HC000001"), get_table_index(user_list, STRING("1HC000001"))); // TODO: Actually add local users to user_list

		channels.len = oldlen;

		if (channels.len <= offset+1)
			break;

		channels.data += offset + 1;
		channels.len -= offset + 1;
	}

	return 0;
}

int client_privmsg_handler(uint64_t argc, struct string *argv) {
	if (argc < 2)
		return 1;

	SEND(STRING(":1HC000001 PRIVMSG "));
	SEND(argv[0]);
	SEND(STRING(" :"));
	SEND(argv[1]);
	SEND(STRING("\n"));

	uint64_t offset;
	if (argv[0].data[0] == '#') {
		if (argv[1].len < command_prefix.len || memcmp(argv[1].data, command_prefix.data, command_prefix.len) != 0)
			return 0;

		offset = command_prefix.len;
	} else {
		offset = 0;
	}

	if (offset >= argv[1].len || argv[1].data[offset] == ' ')
		return 0;

	uint64_t command_argc = 0;
	uint64_t old_offset = offset;
	while (offset < argv[1].len) {
		while (offset < argv[1].len && argv[1].data[offset] != ' ')
			offset++;

		command_argc++;

		while (offset < argv[1].len && argv[1].data[offset] == ' ')
			offset++;
	}
	offset = old_offset;

	struct string command_argv[command_argc]; // argv[0] in this case is the command itself, unlike network command handlers... might change one of these two later to match
	uint64_t i = 0;
	while (offset < argv[1].len) {
		command_argv[i].data = argv[1].data+offset;
		uint64_t start = offset;

		while (offset < argv[1].len && argv[1].data[offset] != ' ')
			offset++;

		command_argv[i].len = offset - start;

		while (offset < argv[1].len && argv[1].data[offset] == ' ')
			offset++;

		i++;
	}

	argv[1].data += old_offset;
	argv[1].len -= old_offset;
	struct command_def *cmd = get_table_index(user_commands, command_argv[0]);
	if (cmd) {
		struct string message[] = {
			STRING("Local user "),
			client_nick,
			STRING(" executes `"),
			argv[1],
			STRING("'"),
		};

		privmsg(STRING("1HC000000"), log_channel, sizeof(message)/sizeof(*message), message);

		return cmd->func(STRING("1HC000001"), argv[1], argv[0], command_argc, command_argv, 1);
	} else {
		if (argv[0].data[0] == '#') {
			SEND(STRING(":1HC000000 NOTICE "));
			SEND(argv[0]);
			SEND(STRING(" :Unknown command: "));
			SEND(command_prefix);
			SEND(command_argv[0]);
			SEND(STRING("\n"));
		}
		return 0;
	}

	return 0;
}

int client_raw_handler(uint64_t argc, struct string *argv) {
	if (argc < 1)
		return 1;

	SEND(argv[0]);
	SEND(STRING("\n"));

	return 0;
}

int client_ping_handler(uint64_t argc, struct string *argv) {
	if (argc < 1)
		return 1;

	SENDCLIENT(STRING(":"));
	SENDCLIENT(server_name);
	SENDCLIENT(STRING(" PONG :"));
	SENDCLIENT(argv[0]);
	SENDCLIENT(STRING("\r\n"));

	return 0;
}

int client_mode_handler(uint64_t argc, struct string *argv) {
	if (argc < 2)
		return 0; // Mode querying not supported yet

	SEND(STRING(":1HC000001 MODE "));
	for (uint64_t i = 0; i < argc - 1; i++) {
		SEND(argv[i]);
		SEND(STRING(" "));
	}
	SEND(STRING(":"));
	SEND(argv[argc - 1]);
	SEND(STRING("\n"));

	return 0;
}

int client_fd = -1;

int client_listen_fd;
int initclientnetwork(void) {
	client_network_commands.array = malloc(0);

	set_table_index(&client_network_commands, STRING("NICK"), &client_nick_handler);
	set_table_index(&client_network_commands, STRING("USER"), &client_user_handler);
	set_table_index(&client_network_commands, STRING("JOIN"), &client_join_handler);
	set_table_index(&client_network_commands, STRING("PRIVMSG"), &client_privmsg_handler);
	set_table_index(&client_network_commands, STRING("RAW"), &client_raw_handler);
	set_table_index(&client_network_commands, STRING("PING"), &client_ping_handler);
	set_table_index(&client_network_commands, STRING("MODE"), &client_mode_handler);

	client_nick.data = malloc(0);

	client_listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (client_listen_fd < 0)
		return 1;

	int one = 1;
	setsockopt(client_listen_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));

	struct sockaddr_in localhost = {
		.sin_family = AF_INET,
		.sin_port = htons(6667),
	};
	inet_pton(AF_INET, "127.0.0.1", &localhost.sin_addr); // this is indeed localhost for mine, and I have no intent to change this
	bind(client_listen_fd, (struct sockaddr*)&localhost, sizeof(localhost));

	listen(client_listen_fd, 1);

	return 0;
}

#if LOGALL
ssize_t SENDCLIENT(struct string msg) {
	if (msg.len == 0 || client_fd == -1)
		return 0;

	static char printprefix = 1;
	if (printprefix) {
#if COLORIZE
		WRITES(1, STRING("\x1b[31m[Us->Client] \x1b[32m"));
#else
		WRITES(1, STRING("[Us->Client] "));
#endif

		printprefix = 0;
	}

	WRITES(1, msg);

	if (msg.data[msg.len - 1] == '\n') {
		printprefix = 1;
#if COLORIZE
		WRITES(1, STRING("\x1b[0m\n"));
#else
		WRITES(1, STRING("\n"));
#endif
	}


	return WRITES(client_fd, msg);
}
#endif
